/*=====================================================================*
 *                   Copyright (C) 2011 Paul Mineiro                   *
 * All rights reserved.                                                *
 *                                                                     *
 * Redistribution and use in source and binary forms, with             *
 * or without modification, are permitted provided that the            *
 * following conditions are met:                                       *
 *                                                                     *
 *     * Redistributions of source code must retain the                *
 *     above copyright notice, this list of conditions and             *
 *     the following disclaimer.                                       *
 *                                                                     *
 *     * Redistributions in binary form must reproduce the             *
 *     above copyright notice, this list of conditions and             *
 *     the following disclaimer in the documentation and/or            *
 *     other materials provided with the distribution.                 *
 *                                                                     *
 *     * Neither the name of Paul Mineiro nor the names                *
 *     of other contributors may be used to endorse or promote         *
 *     products derived from this software without specific            *
 *     prior written permission.                                       *
 *                                                                     *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND              *
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,         *
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES               *
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE             *
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER               *
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,                 *
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES            *
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE           *
 * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR                *
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF          *
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT           *
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY              *
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE             *
 * POSSIBILITY OF SUCH DAMAGE.                                         *
 *                                                                     *
 * Contact: Paul Mineiro <paul@mineiro.com>                            *
 *=====================================================================*/

#ifndef __MODEL_HH__
#define __MODEL_HH__

#include <cerrno>
#include <cstring>
#include <stdexcept>
#include <stdint.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <utility>

namespace playerpiano
{
  class Model
    {
      private:
        static const uint64_t vnum = 0x3081f53623ee0b41ULL;
        static const uint64_t max_entries = 1024;
        static const uint64_t namelen = 16;

        static inline uint64_t
        roundup8 (uint64_t l)
          {
            if (l % 8 == 0)
              {
                return l;
              }
            else
              {
                return l + 8 - (l % 8);
              }
          }

        struct Entry
          {
            char     name[namelen];
            uint64_t offset;
            uint64_t orig_length;
            uint64_t actual_length;
          };

        struct Header
          {
            uint64_t version;
            uint64_t size;
            uint64_t n_entries;
            Entry    entries[max_entries];

            Header () : version (vnum),
                        size (sizeof (Header)),
                        n_entries (0)
              {
                memset (entries, 0, sizeof (entries));
              }
          };

        int fd;
        uint8_t* base;
        Header* header;
        bool read_only;

        void 
        initialize_db_from_fd ()
          {
            Header tmp_hdr;

            if (lseek (fd, 0, SEEK_SET) == (off_t) -1 || 
                read (fd, &tmp_hdr, sizeof (tmp_hdr)) < 
                (ssize_t) sizeof (tmp_hdr))
              {
                throw std::runtime_error (strerror (errno));
              }

            if (tmp_hdr.version != vnum)
              {
                throw std::runtime_error ("model version mismatch");
              }
              
            if (lseek (fd, 0, SEEK_SET) == (off_t) -1)
              {
                throw std::runtime_error (strerror (errno));
              }

            base = 
              reinterpret_cast<uint8_t*> 
                (mmap (NULL,
                       tmp_hdr.size,
                       PROT_READ | (read_only ? 0 : PROT_WRITE),
                       MAP_SHARED,
                       fd,
                       0));

            if (base == reinterpret_cast<uint8_t*> (-1))
              {
                throw std::runtime_error (strerror (errno));
              }

            header = reinterpret_cast<Header*> (base);
          }

        void
        ensure_size (uint64_t new_size)
          {
            if (header->size < new_size)
              {
                char zero = 0;

                if (lseek (fd, new_size, SEEK_SET) == (off_t) -1 ||
                    write (fd, &zero, 1) != (ssize_t) 1)
                  {
                    throw std::runtime_error (strerror (errno));
                  }

                msync (base, header->size, MS_ASYNC);
                munmap (base, header->size);

                base = 
                  reinterpret_cast<uint8_t*> 
                    (mmap (NULL,
                           new_size,
                           PROT_READ | (read_only ? 0 : PROT_WRITE),
                           MAP_SHARED,
                           fd,
                           0));

                if (base == reinterpret_cast<uint8_t*> (-1))
                  {
                    throw std::runtime_error (strerror (errno));
                  }
    
                header = reinterpret_cast<Header*> (base);
                header->size = new_size;
              }
          }

      public:
        class Open
          {
            private:
                std::string pathname;
                bool        is_read_only;

            public:
                Open (const char* _pathname)
                  : pathname (_pathname),
                    is_read_only (true)
                  {
                  }

                Open (const std::string& _pathname)
                  : pathname (_pathname),
                    is_read_only (true)
                  {
                  }

                Open& 
                read_only (bool value)
                  {
                    is_read_only = value;
                    return *this;
                  }

            friend class Model;
          };

        Model (const Open& params)
          {
            read_only = params.is_read_only;

            fd = open (params.pathname.c_str (),
                       params.is_read_only ? O_RDONLY : (O_RDWR | O_CREAT),
                       S_IRUSR | S_IWUSR | 
                       S_IRGRP | S_IWGRP | 
                       S_IROTH | S_IWOTH);

            if (fd < 0)
              {
                throw std::runtime_error (strerror (errno));
              }

            try
              {
                off_t end = lseek (fd, 0, SEEK_END);
                if (end == (off_t) -1)
                  {
                    throw std::runtime_error (strerror (errno));
                  }
                else if (end == 0)
                  {
                    Header tmp_hdr;

                    if (ftruncate (fd, tmp_hdr.size) < 0 ||
                        lseek (fd, 0, SEEK_SET) == (off_t) -1 ||
                          write (fd, &tmp_hdr, sizeof (tmp_hdr)) 
                        < (ssize_t) sizeof (tmp_hdr))
                      {
                        throw std::runtime_error (strerror (errno));
                      }
                  }

                initialize_db_from_fd ();
              }
            catch (...)
              {
                close (fd);
                throw;
              }
          }

        std::pair<void*, uint64_t>
        lookup (const std::string& name)
          {
            for (unsigned int i = 0; i < header->n_entries; ++i)
              {
                if (! strncmp (header->entries[i].name, name.c_str (), namelen))
                  {
                    void* p = base + header->entries[i].offset;
                    return std::make_pair (p, header->entries[i].orig_length);
                  }
              }

            return std::make_pair<void*, uint64_t> (0, 0);
          }

        Model&
        ensure (const std::string& name,
                uint64_t           length)
          {
            if (! lookup (name).first)
              {
                if (read_only)
                  {
                    throw std::runtime_error ("read only violation");
                  }

                if (header->n_entries >= max_entries)
                  {
                    throw std::runtime_error ("too many parameters");
                  }

                strncpy (header->entries[header->n_entries].name,
                         name.c_str (),
                         namelen);

                if (header->n_entries == 0)
                  {
                    header->entries[0].offset = sizeof (Header);
                  }
                else
                  {
                    header->entries[header->n_entries].offset = 
                      header->entries[header->n_entries - 1].offset +
                      header->entries[header->n_entries - 1].actual_length;
                  }

                header->entries[header->n_entries].orig_length = length;
                header->entries[header->n_entries].actual_length = roundup8 (length);
                ensure_size (header->entries[header->n_entries].offset +
                             header->entries[header->n_entries].actual_length);

                ++header->n_entries;
              }

            return *this;
          }

        ~Model ()
          {
            msync (base, header->size, MS_ASYNC);
            munmap (base, header->size);
            close (fd);
          }
    };
}

#endif // __MODEL_HH__
